package com.googlecode.tapestry5validator.hibernate;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import org.apache.tapestry5.Asset;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.Field;
import org.apache.tapestry5.FieldValidator;
import org.apache.tapestry5.internal.services.CompositeFieldValidator;
import org.apache.tapestry5.ioc.AnnotationProvider;
import org.apache.tapestry5.ioc.Invocation;
import org.apache.tapestry5.ioc.MessageFormatter;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.MethodAdvice;
import org.apache.tapestry5.ioc.MethodAdviceReceiver;
import org.apache.tapestry5.ioc.OrderedConfiguration;
import org.apache.tapestry5.ioc.annotations.Order;
import org.apache.tapestry5.ioc.annotations.SubModule;
import org.apache.tapestry5.ioc.services.ThreadLocale;
import org.apache.tapestry5.services.AssetSource;
import org.apache.tapestry5.services.FieldValidatorDefaultSource;
import org.apache.tapestry5.services.FormSupport;
import org.hibernate.util.StringHelper;
import org.hibernate.validator.Validator;
import org.hibernate.validator.ValidatorClass;
import org.hibernate.validator.interpolator.DefaultMessageInterpolator;
import org.slf4j.Logger;

import com.googlecode.tapestry5validator.hibernate.adapters.CreditCardNumberAdapter;
import com.googlecode.tapestry5validator.hibernate.adapters.DigitsAdapter;
import com.googlecode.tapestry5validator.hibernate.adapters.EANAdapter;
import com.googlecode.tapestry5validator.hibernate.adapters.EmailAdapter;
import com.googlecode.tapestry5validator.hibernate.adapters.FutureAdapter;
import com.googlecode.tapestry5validator.hibernate.adapters.LengthAdapter;
import com.googlecode.tapestry5validator.hibernate.adapters.MaxAdapter;
import com.googlecode.tapestry5validator.hibernate.adapters.MinAdapter;
import com.googlecode.tapestry5validator.hibernate.adapters.NotNullAdapter;
import com.googlecode.tapestry5validator.hibernate.adapters.PastAdapter;
import com.googlecode.tapestry5validator.hibernate.adapters.PatternAdapter;
import com.googlecode.tapestry5validator.hibernate.adapters.RangeAdapter;
import com.googlecode.tapestry5validator.hibernate.validators.CommonFieldValidator;

/**
 * Provide support hibernate validator on Tapestry 5. Use {@link SubModule}
 * for enable this module in your project.
 * 
 * @author Yesrastov Valentin
 */
public class TapestryHibernateValidatorModule {

	
	public static final String DEFAULT_VALIDATOR_MESSAGE = 
		"org.hibernate.validator.resources.DefaultValidatorMessages";
	
	
	public static final String VALIDATOR_MESSAGE = "ValidatorMessages";

	public static SupportedHibernateValidatorsProvider buildSupportedHibernateValidatorsProvider(
	        List<ValidatorAdapter> elements) {
		return new SupportedHibernateValidatorsProvider(elements);
	}

	
	/**
	 * Add default hibernate validators adapters 
	 */
	public static void contributeSupportedHibernateValidatorsProvider(
	        OrderedConfiguration<ValidatorAdapter> configuration) {
		
		addToConfigation(new CreditCardNumberAdapter(), configuration);
		addToConfigation(new DigitsAdapter(), configuration);
		addToConfigation(new EANAdapter(), configuration);
		addToConfigation(new EmailAdapter(), configuration);
		addToConfigation(new FutureAdapter(), configuration);
		addToConfigation(new LengthAdapter(), configuration);
		addToConfigation(new MaxAdapter(), configuration);
		addToConfigation(new MinAdapter(), configuration);
		addToConfigation(new NotNullAdapter(), configuration);
		addToConfigation(new PatternAdapter(), configuration);
		addToConfigation(new PastAdapter(), configuration);
		addToConfigation(new RangeAdapter(), configuration);
	}
	
	/**
	 * Method-helper for add hibernate validators adapters
	 */
	private static void addToConfigation(ValidatorAdapter element, 
	                                     OrderedConfiguration<ValidatorAdapter> configuration) {
		configuration.add(element.getValidatorType(), element);
	}
	
	/**
	 * Add javascript file 'tapestry-validator.js' in client infrastructure, 
	 * for extending tapestry client validation. 
	 */
	@SuppressWarnings("unchecked")
	@Order("after:*")
	public static void adviseClientInfrastructure(MethodAdviceReceiver receiver,
	                                              final AssetSource assetSource, 
	                                              final ThreadLocale threadLocale) {

		MethodAdvice advice =
			new MethodAdvice() {

				@Override
				public void advise(Invocation invocation) {
					invocation.proceed();
					List<Asset> assets = (List<Asset>) invocation.getResult();
					if (assets != null && !assets.isEmpty()) {
						
					Asset jsValidator = assetSource
						.getAsset(	null,
									"com/googlecode/tapestry5validator/tapestry-validator.js",
									threadLocale.getLocale());
					
						assets.add(jsValidator);
					}
					
					invocation.overrideResult(assets);
				}
			
			};
			
			Method method;
			
			try {
				method = receiver.getInterface().getMethod("getJavascriptStack");
			} catch (SecurityException e) {
				throw new RuntimeException(e);	
			} catch (NoSuchMethodException e) {
				throw new RuntimeException(String.format("Can't find method FieldValidatorDefaultSource" +
				                     					".createDefaultValidator(Field, String, Messages, Locale, " +
				                    					"Class, AnnotationProvider). Changed API?", ""), e);
			}
		
			receiver.adviseMethod(method, advice);
	}
	
			
	/**
	 * Add advice for including hibernate validators to list field validators.
	 * <p> 
	 * Advice add to  {@link FieldValidatorDefaultSource#createDefaultValidator(ComponentResources, String)}
	 * and to {@link FieldValidatorDefaultSource#createDefaultValidator(Field, String, Messages, Locale, Class, AnnotationProvider)}
	 * methods.
	 */
	@SuppressWarnings("unchecked")
	@Order("after:*")
	public static void adviseFieldValidatorDefaultSource(MethodAdviceReceiver receiver,
	                                                     final FormSupport formSupport,
	                                                     final SupportedHibernateValidatorsProvider validatorsProvider,
	                                                     final Logger log) {
		MethodAdvice advice =
			new MethodAdvice() {
	
				@Override
				@SuppressWarnings("unused")
				public void advise(Invocation invocation) {
					
					invocation.proceed();
					
					//PARAMETRS
					Field field = (Field) invocation.getParameter(0);
					String overrideId = (String) invocation.getParameter(1);
			        Messages overrideMessages = (Messages) invocation.getParameter(2);
			        Locale locale = (Locale) invocation.getParameter(3);
			        Class propertyType = (Class) invocation.getParameter(4);
			        AnnotationProvider propertyAnnotations = (AnnotationProvider) invocation.getParameter(5);

					Collection<FieldValidator> hibernateValidators = 
						createHibernateValidators(field,
						                          overrideId,
						                          formSupport,
						                          overrideMessages,
						                          locale,
						                          propertyAnnotations,
						                          validatorsProvider,
						                          log);
					
					applyHibernateValidators(invocation, hibernateValidators);
					
				}
	
			};
			
		Method method = findMethod(	receiver.getInterface(),
									"createDefaultValidator",
									Field.class,
									String.class,
									Messages.class,
									Locale.class,
									Class.class,
									AnnotationProvider.class);			
		
			
		receiver.adviseMethod(method, advice);
		
		advice = new MethodAdvice() {

			@Override
			public void advise(Invocation invocation) {
				invocation.proceed();
				
				//PARAMETRS
				ComponentResources resources = (ComponentResources) invocation.getParameter(0); 
				String parameterName = (String) invocation.getParameter(1);

		        Class propertyType = resources.getBoundType(parameterName);

		        if (propertyType == null) return;

		        Field field = (Field) resources.getComponent();
				
				Collection<FieldValidator> hibernateValidators = 
					createHibernateValidators(field,
					                          resources.getId(),
					                          formSupport,
					                          resources.getContainerMessages(),
					                          resources.getLocale(),
					                          resources.getAnnotationProvider(parameterName),
					                          validatorsProvider,
					                          log);	
				
				applyHibernateValidators(invocation, hibernateValidators);
			}
			
		};
		
		method = findMethod(receiver.getInterface(),
							"createDefaultValidator",
							ComponentResources.class,
							String.class);

		receiver.adviseMethod(method, advice);
		
	}

	@SuppressWarnings("unchecked")
	private static void applyHibernateValidators(Invocation invocation,
	                                             Collection<FieldValidator> hibernateValidators) {
		if (!hibernateValidators.isEmpty()) {

			FieldValidator result = (FieldValidator) invocation.getResult();

			List<FieldValidator> validators = new ArrayList<FieldValidator>();

			validators.add(result);
			validators.addAll(hibernateValidators);

			CompositeFieldValidator newResult = new CompositeFieldValidator(validators);

			invocation.overrideResult(newResult);

		}
	}	
	
	/**
	 * Method-helper for find method in class.
	 */
	private static Method findMethod(Class<?> clazz, String methodName, Class<?>...params) {
		try {
			return clazz.getMethod(methodName, params);
		} catch (SecurityException e) {
			throw new RuntimeException(e);	
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(String.format("Can't find method %s"+ 
			                     					".%s(%s). Changed API?", 
			                     					clazz.getSimpleName(),
			                     					methodName,
			                     					params), e);
		}
	}
	
	/**
	 * Create hibernate validators for current form field.
	 */
	@SuppressWarnings("unchecked")
	private static Collection<FieldValidator> 
			createHibernateValidators(final Field field,
			                          String overrideId,
			                          FormSupport formSupport,
			                          Messages overrideMessages,
			                          Locale locale,
			                          AnnotationProvider propertyAnnotations, 
			                          SupportedHibernateValidatorsProvider validatorsProvider, 
			                          Logger log) {
		
		if (locale == null) {
			locale = Locale.getDefault();
		}

		List<FieldValidator> result = new ArrayList<FieldValidator>();

		ResourceBundle messageBundle = getDefaultResourceBundle(log, locale);
		ResourceBundle defaultMessageBundle = ResourceBundle.getBundle( DEFAULT_VALIDATOR_MESSAGE , locale);
		

 
		for (ValidatorAdapter se : validatorsProvider.getSupportedValidators()) {
			Class<? extends Annotation> ac = se.getAnnotationClass();
			Annotation a = propertyAnnotations.getAnnotation(ac);
			if (a == null) {
				continue;
			}
			Map<String, Object> annotationParameters = getAnnotationParameters(a, log);
			
			Validator validator = createValidator(a);
			//Пытаемся найти в переопределенных сообщения тапестри
			//Находим то форматируем то что нашили с подстановкой имени поля, 
			//и отдаем на интерполяцию хибернейту. 
			//Если нет в тапестри пытаемся найти бандл ValidationMessage
			//Если там нет отдаем дефолтному бандлу
			DefaultMessageInterpolator defaultInterpolator = 
				new DefaultMessageInterpolator();	
				
			defaultInterpolator.initialize( messageBundle, defaultMessageBundle  );
			
			MessageFormatter formatter = findOverridedFormatter(formSupport.getFormValidationId(),
			                                                    overrideId,
			                                                    overrideMessages,
			                                                    se.getValidatorType()); //hvmax например
			String toInterpolate;
			if (formatter != null) {
				toInterpolate = formatter.format(field.getLabel());
			} else {
				try {
					toInterpolate = (String) ac.getMethod("message").invoke(a);
				} catch (Exception e) {
					throw new RuntimeException(String.format("Could not get value of message method from annotation %s", a));
				}
			}
			
			defaultInterpolator.initialize(a, null);
			String message = defaultInterpolator.interpolate(toInterpolate, validator, null);
			
			//TODO: Cделать конфигурируемым.
			if (formatter == null) {
				if (!startWithUpper(message)) {
					message = field.getLabel()+" "+message;	
				} 
			}
			
			//TODO: Cделать конфигурируемым.
			if (!message.endsWith(".")) {
				message += ".";
			}
			
			result.add(new CommonFieldValidator(field, 
			                                    formSupport, 
			                                    se.getJSMethod(), 
			                                    validator, 
			                                    message, 
			                                    se.fetchToJSParametrs(annotationParameters),
			                                    se.isRequired()));
		}
		
		return result;
	}

	private static boolean startWithUpper(String message) {
		if (message == null || message.isEmpty()) {
			return false;
		}
		//Я верю что это можно сделать проще
		String first = message.charAt(0)+"";
		
		return first.equals( first.toUpperCase() );
	}


	private static Map<String, Object> getAnnotationParameters(Annotation annotation, Logger log) {
		Class<?> clazz = annotation.annotationType();
		Map<String, Object> annotationParameters = new HashMap<String, Object>();
		for ( Method method  : clazz.getDeclaredMethods() ) {
			try {
				if ( method.getReturnType() != void.class
						&& method.getParameterTypes().length == 0
						&& ! Modifier.isStatic( method.getModifiers() )
						&& ! method.getName().equals("message")) {
					
					
					//cannot use an exclude list because the parameter name could match a method name
					annotationParameters.put( method.getName(), method.invoke( annotation ) );
				}
			}
			catch (IllegalAccessException e) {
				//really should not happen, but we degrade nicely
				log.warn( "Unable to access {}", StringHelper.qualify( clazz.toString(), method.getName() ) );
			}
			catch (InvocationTargetException e) {
				//really should not happen, but we degrade nicely
				log.warn( "Unable to access {}", StringHelper.qualify( clazz.toString(), method.getName() ) );
			}
		}
		return annotationParameters;
	}

	private static MessageFormatter findOverridedFormatter(	String formId,
															String overrideId,
															Messages overrideMessages,
															String validatorType) {

		String overrideKey = formId + "-" + overrideId + "-" + validatorType
				+ "-message";

		if (overrideMessages.contains(overrideKey))
			return overrideMessages.getFormatter(overrideKey);

		overrideKey = overrideId + "-" + validatorType + "-message";

		if (overrideMessages.contains(overrideKey))
			return overrideMessages.getFormatter(overrideKey);

		return null;
	}
	
	/**
	 * Copy paste from {@link org.hibernate.validator.ClassValidator}
	 */
	private static ResourceBundle getDefaultResourceBundle(Logger log, Locale locale) {
		ResourceBundle rb;
		try {
			//use context class loader as a first citizen
			ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
			if ( contextClassLoader == null ) {
				throw new MissingResourceException( "No context classloader", null, VALIDATOR_MESSAGE );
			}
			rb = ResourceBundle.getBundle(
					VALIDATOR_MESSAGE,
					locale,
					contextClassLoader
			);
		}
		catch (MissingResourceException e) {
			log.trace( "ResourceBundle {} not found in thread context classloader", VALIDATOR_MESSAGE );
			//then use the Validator Framework classloader
			try {
				rb = ResourceBundle.getBundle(
						VALIDATOR_MESSAGE,
						locale,
						TapestryHibernateValidatorModule.class.getClassLoader()
				);
			}
			catch (MissingResourceException ee) {
				log.debug(
						"ResourceBundle ValidatorMessages not found in Validator classloader. Delegate to {}",
						DEFAULT_VALIDATOR_MESSAGE
				);
				//the user did not override the default ValidatorMessages
				rb = null;
			}
		}
		return rb;
	}
	
	@SuppressWarnings("unchecked")
	private static Validator createValidator(Annotation annotation) {
		try {
			ValidatorClass validatorClass = annotation.annotationType()
				.getAnnotation( ValidatorClass.class );
			
			if ( validatorClass == null ) {
				throw new IllegalArgumentException(String.format("Annotation %s is not Validator Annotation." +
						"Define Validator class for this annotation, using @ValidatorClass annotation", annotation));
			}
			Validator beanValidator = validatorClass.value().newInstance();
			beanValidator.initialize( annotation );
			return beanValidator;
		}
		catch (Exception e) {
			throw new IllegalArgumentException( "Could not instantiate validator", e );
		}
	}

	
}
